package com.atguigu.gmall.common.redis;

import com.atguigu.gmall.common.constant.RedisConst;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 缓存+分布式锁功能对自定义注解增强切面类
 *
 * @author: atguigu
 * @create: 2023-09-05 14:18
 */
@Slf4j
@Aspect
@Component
public class GmallCacheAspect {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private RedissonClient redissonClient;


    /**
     * 环绕通知由前置通知+目标方法+后置通知
     * 前置通知：1.查询Redis缓存 2.获取分布所锁
     * 目标方法：执行查询数据库方法
     * 后缀通知：释放锁
     *
     * @param pjp 切入点对象
     */
    @Around("@annotation(com.atguigu.gmall.common.redis.GmallCache)")
    public Object getDataFromCacheByLock(ProceedingJoinPoint pjp) throws Throwable {
        Object dataResult = new Object();
        try {
            //一、执行前置通知
            log.info("前置通知");
            //1.优先从Redis缓存中获取业务数据
            //1.1 构建业务数据Key 形式（prefix:param:suffix） 前缀/后缀通过注解获取 业务参数：从目标方法获取
            //1.1.1 通过pjp切入点对象获取方法签名（特征）
            MethodSignature signature = (MethodSignature) pjp.getSignature();
            Method method = signature.getMethod();

            //1.1.2 通过方法签名获取方法上注解得到注解对象
            GmallCache gmallCache = method.getAnnotation(GmallCache.class);

            //1.1.3 通过注解对象获取注解中属性 前缀/后缀 构建业务key
            String prefix = gmallCache.prefix();
            String suffix = gmallCache.suffix();

            //1.1.4 获取目标方法上参数
            String param = "none";
            Object[] args = pjp.getArgs();
            if (args != null && args.length > 0) {
                //将方法参数从数组转为集合
                List<Object> paramList = Arrays.asList(args);
                param = paramList.stream().map(arg -> {
                    return arg.toString();
                }).collect(Collectors.joining(":"));
            }
            String dataKey = prefix + param + suffix;

            //1.2 获取业务数据
            dataResult = redisTemplate.opsForValue().get(dataKey);
            if (dataResult != null) {
                return dataResult;
            }

            //2.尝试获取分布式锁-Redisson实现分布式锁
            //2.1 构建业务数据锁key
            String lockKey = prefix + param + RedisConst.SKULOCK_SUFFIX;
            //2.2 通关redissonClient获取锁对象
            RLock lock = redissonClient.getLock(lockKey);
            //2.3 尝试获取分布式锁
            //boolean flag = lock.tryLock(RedisConst.SKULOCK_EXPIRE_PX1, -1, TimeUnit.SECONDS);
            //if (flag) {
            lock.lock();
            try {
                //2.4 避免获取分布所失败且处于等待状态线程获取到分布式锁后再次执行查数据库代码，故加一个策略：当获取到分布式锁后，再查询一次缓存
                dataResult = redisTemplate.opsForValue().get(dataKey);
                if (dataResult != null) {
                    return dataResult;
                }
                //3.获取分布式锁成功执行业务方法
                //二、执行目标方法(执行业务数据查库方法)
                dataResult = pjp.proceed();
                if (dataResult != null) {
                    int randomTimeSec = new Random().nextInt(1000);
                    redisTemplate.opsForValue().set(dataKey, dataResult, RedisConst.SKUKEY_TIMEOUT + randomTimeSec, TimeUnit.SECONDS);
                    return dataResult;
                }
            } finally {
                //三、执行后置通知
                log.info("后置通知");
                //4.将分布式锁释放
                lock.unlock();
            }
            //} else {
            //    //5.获取分布式失败-自旋尝试再次获取
            //    Thread.sleep(500);
            //    return this.getDataFromCacheByLock(pjp);
            //}
        } catch (Throwable e) {
            //6.兜底处理方案：查询数据库(目标方法)
            return pjp.proceed();
        }
        return null;
    }
}
